package name.fordj;

import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.util.concurrent.LockFactory;
import ca.odell.glazedlists.util.concurrent.ReadWriteLock;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: Jeremy
 * Date: Mar 7, 2008
 * Time: 11:05:57 PM
 */
public class Indexer<T> {
    public static final String INDEX_ADDED = "indexAdded";
    public static final String INDEX_REMOVED = "indexRemoved";
    public static final String ITEM_ADDED = "itemAdded";
    public static final String ITEM_REMOVED = "itemRemoved";

    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private Class<T> clazz;
    private EventList<T> source;

    private List<T> sourceProxy = new ArrayList<T>();

    private BeanPropertyFilterator<T> filterator;
    private final ReadWriteLock readWriteLock = LockFactory.DEFAULT.createReadWriteLock();


    private static class ValueKey {
        private final String property;
        private final Object value;

        private ValueKey(String property, Object value) {
            this.property = property;
            this.value = value;
        }

        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            ValueKey valueKey = (ValueKey) o;

            if (!property.equals(valueKey.property)) return false;
            if (!value.equals(valueKey.value)) return false;

            return true;
        }

        public int hashCode() {
            int result;
            result = property.hashCode();
            result = 31 * result + value.hashCode();
            return result;
        }
    }

    private Map<T, List<Object>> itemToIndexValues = new HashMap<T, List<Object>>();
    private Map<ValueKey, List<List<Object>>> valueKeyToValueListMap = new HashMap<ValueKey, List<List<Object>>>();
    private Map<List<Object>, T> valueListToItemMap = new IdentityHashMap<List<Object>,T>();

    private ListEventListener<T> sourceListener = new ListEventListener<T>() {
        public void listChanged(ListEvent<T> listChanges) {
            while(listChanges.next()) {
                final int type = listChanges.getType();
                final int index = listChanges.getIndex();

                if(type == ListEvent.INSERT) {
                    T item = source.get(index);
                    sourceProxy.add(index, item);

                    index(filterator, filterator.getProperties(), item);
                } else if(type == ListEvent.DELETE) {
                    T item = sourceProxy.remove(index);
                    removeItemFromIndex(item);
                } else if(type == ListEvent.UPDATE) {
                    T proxy = sourceProxy.get(index);
                    T orig = source.get(index);
                    if(proxy != orig) {
                        removeItemFromIndex(proxy);
                        sourceProxy.set(index, orig);
                    }
                    removeItemFromIndex(orig);
                    index(filterator, filterator.getProperties(), orig);
                }
            }
        }
    };

    private void removeItemFromIndex(T item) {
        boolean unlocked = false;
        try {
            readWriteLock.writeLock().lock();

            List<Object> values = itemToIndexValues.remove(item);
            if(values != null) {
                valueListToItemMap.remove(values);
                String[] properties = filterator.getProperties();
                for (String property : properties) {
                    for (Object value : values) {
                        if(value != null) {
                            ValueKey vk = new ValueKey(property, value);
                            List<List<Object>> listOfValues = valueKeyToValueListMap.get(vk);
                            if(listOfValues != null) {
                                listOfValues.remove(values);
                                if(listOfValues.isEmpty()) {
                                   valueKeyToValueListMap.remove(vk);
                                }
                            }
                        }
                    }
                }

                readWriteLock.writeLock().unlock();
                unlocked = true;
                support.firePropertyChange(ITEM_REMOVED, null, item);
            }
        } finally {
            if(!unlocked) {
                readWriteLock.writeLock().unlock();
            }
        }
    }

    public Indexer(Class<T> clazz, EventList<T> source, String... properties) {
        this.clazz = clazz;
        this.source = source;
        this.filterator = new BeanPropertyFilterator<T>(clazz, properties);

        try {
            source.getReadWriteLock().readLock().lock();
            this.sourceProxy.addAll(source);
        } finally {
            source.getReadWriteLock().readLock().unlock();
        }
        this.source.addListEventListener(sourceListener);

        index(filterator);
    }

    public void dispose() {
        this.itemToIndexValues.clear();
        this.valueListToItemMap.clear();
        this.valueKeyToValueListMap.clear();
    }

    public void addIndex(String property) {
        Set<String> properties = new HashSet<String>(Arrays.asList(filterator.getProperties()));
        if(properties.contains(property)) {
            throw new IllegalArgumentException("Property " + property + " is already indexed");
        }
        properties.add(property);
        filterator = new BeanPropertyFilterator<T>(clazz, properties.toArray(new String[properties.size()]));

        //index(new BeanPropertyFilterator<T>(clazz, property));
        index(filterator);
        support.firePropertyChange(INDEX_ADDED, null, property);
    }

    public boolean hasIndex(String property) {
        for (String p : filterator.getProperties()) {
            if(p.equals(property)) {
                return true;
            }
        }
        return false;
    }

    public void removeIndex(String property) {
        try {
            readWriteLock.writeLock().lock();

            Set<String> properties = new HashSet<String>(Arrays.asList(filterator.getProperties()));
            if(!properties.contains(property)) {
                throw new IllegalArgumentException("Property " + property + " is not indexed");
            }
            properties.remove(property);
            filterator = new BeanPropertyFilterator<T>(clazz, properties.toArray(new String[properties.size()]));

            index(filterator);

        } finally {
            readWriteLock.writeLock().unlock();
        }
        support.firePropertyChange(INDEX_REMOVED, null, property);
    }

    private void index(BeanPropertyFilterator<T> bp) {
        try {
            source.getReadWriteLock().readLock().lock();

            String[] properties = bp.getProperties();
            for (T t : source) {
                index(bp, properties, t);
            }
        } finally {
            source.getReadWriteLock().readLock().unlock();
        }
    }

    private void index(BeanPropertyFilterator<T> bp, String[] properties, T t) {
        try {
            readWriteLock.writeLock().lock();
            BasicEventList<Object> baseList = new BasicEventList<Object>();
            bp.getFilterValues(baseList, t);

            for(int i=0; i<properties.length; ++i) {
                String property = properties[i];
                Object value = baseList.get(i);
                if(value != null) {
                    ValueKey vk = new ValueKey(property, value);
                    List<List<Object>> lists = valueKeyToValueListMap.get(vk);
                    if(lists == null) {
                        lists = new ArrayList<List<Object>>();
                        valueKeyToValueListMap.put(vk, lists);
                    }
                    lists.add(baseList);
                }
            }

            valueListToItemMap.put(baseList, t);
            itemToIndexValues.put(t, baseList);
        } finally {
            readWriteLock.writeLock().unlock();
        }
        support.firePropertyChange(ITEM_ADDED, null, t);
    }

    public List<T> search(String property, Object value) {
        List<T> results = new ArrayList<T>();
        try {
            readWriteLock.readLock().lock();
            ValueKey vk = new ValueKey(property, value);
            List<List<Object>> values = valueKeyToValueListMap.get(vk);
            if(values != null) {
                for (List<Object> objects : values) {
                    results.add(valueListToItemMap.get(objects));
                }
            }
        } finally {
            readWriteLock.readLock().unlock();
        }

        return results;
    }

    public T getValueByIndex(String property, Object value) {
        T result = null;

        try {
            readWriteLock.readLock().lock();
            //TODO: come up with better way to do this?
            boolean found = false;
            for (String p : filterator.getProperties()) {
                if(p.equals(property)) {
                   found = true;
                    break;
                }
            }
            if(!found) {
                throw new IllegalArgumentException("Failed to find index " + property);
            }

            ValueKey vk = new ValueKey(property, value);
            List<List<Object>> values = valueKeyToValueListMap.get(vk);
            if(values != null && values.size() > 0) {
                List<Object> valueList = values.get(0);
                result = this.valueListToItemMap.get(valueList);
            }
        } finally {
            readWriteLock.readLock().unlock();
        }
        return result;
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    }
}
